<?php

namespace app\generators;

use ReflectionClass;
use Yii;
use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\helpers\FileHelper;
use yii\helpers\Inflector;
use yii\helpers\VarDumper;
use yii\web\View;

/**
 * This is the base class for all generator classes.
 *
 * A generator instance is responsible for taking user inputs, validating them,
 * and using them to generate the corresponding code based on a set of code template files.
 *
 * A generator class typically needs to implement the following methods:
 *
 * - [[getName()]]: returns the name of the generator
 * - [[getDescription()]]: returns the detailed description of the generator
 * - [[generate()]]: generates the code based on the current user input and the specified code template files.
 * This is the place where main code generation code resides.
 *
 * @property string $description The detailed description of the generator. This property is read-only.
 * @property string $stickyDataFile The file path that stores the sticky attribute values. This property is
 *           read-only.
 * @property string $templatePath The root path of the template files that are currently being used. This
 *           property is read-only.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
abstract class Generator extends Model
{

  /**
   *
   * @var array a list of available code templates. The array keys are the template names,
   *      and the array values are the corresponding template paths or path aliases.
   */
  public $templates = [];

  /**
   *
   * @var string the name of the code template that the user has selected.
   *      The value of this property is internally managed by this class.
   */
  public $template = 'default';

  /**
   *
   * @var bool whether the strings will be generated using `Yii::t()` or normal strings.
   */
  public $enableI18N = true;

  /**
   *
   * @var string the message category used by `Yii::t()` when `$enableI18N` is `true`.
   *      Defaults to `app`.
   */
  public $messageCategory = 'app';

  const AFTER_SAVE_SUCCESS = 'afterSaveSuccess';

  /**
   *
   * @return string name of the code generator
   */
  abstract public function getName();

  /**
   * Generates the code based on the current user input and the specified code template files.
   * This is the main method that child classes should implement.
   * Please refer to [[\yii\gii\generators\controller\Generator::generate()]] as an example
   * on how to implement this method.
   *
   * @return CodeFile[] a list of code files to be created.
   */
  abstract public function generate();

  /**
   *
   * {@inheritdoc}
   */
  public function init()
  {

    parent::init();
    if (!isset($this->templates['default'])) {
      $this->templates['default'] = $this->defaultTemplate();
    }
    foreach ($this->templates as $i => $template) {
      $this->templates[$i] = Yii::getAlias($template);
    }
  }

  /**
   *
   * {@inheritdoc}
   */
  public function attributeLabels()
  {

    return [
      'enableI18N' => 'Enable I18N',
      'messageCategory' => 'Message Category'
    ];
  }

  /**
   * Returns a list of code template files that are required.
   * Derived classes usually should override this method if they require the existence of
   * certain template files.
   *
   * @return array list of code template files that are required. They should be file paths
   *         relative to [[templatePath]].
   */
  public function requiredTemplates()
  {

    return [];
  }

  /**
   * Returns the list of sticky attributes.
   * A sticky attribute will remember its value and will initialize the attribute with this value
   * when the generator is restarted.
   *
   * @return array list of sticky attributes
   */
  public function stickyAttributes()
  {

    return [
      'template',
      'enableI18N',
      'messageCategory'
    ];
  }

  /**
   * Returns the list of hint messages.
   * The array keys are the attribute names, and the array values are the corresponding hint messages.
   * Hint messages will be displayed to end users when they are filling the form for the generator.
   *
   * @return array the list of hint messages
   */
  public function hints()
  {

    return [
      'enableI18N' => 'This indicates whether the generator should generate strings using <code>Yii::t()</code> method.
                Set this to <code>true</code> if you are planning to make your application translatable.',
      'messageCategory' => 'This is the category used by <code>Yii::t()</code> in case you enable I18N.'
    ];
  }

  /**
   * Returns the list of auto complete values.
   * The array keys are the attribute names, and the array values are the corresponding auto complete values.
   * Auto complete values can also be callable typed in order one want to make postponed data generation.
   *
   * @return array the list of auto complete values
   */
  public function autoCompleteData()
  {

    return [];
  }

  /**
   * Returns the message to be displayed when the newly generated code is saved successfully.
   * Child classes may override this method to customize the message.
   *
   * @return string the message to be displayed when the newly generated code is saved successfully.
   */
  public function successMessage()
  {

    return 'The code has been generated successfully.';
  }

  /**
   * Returns the view file for the input form of the generator.
   * The default implementation will return the "form.php" file under the directory
   * that contains the generator class file.
   *
   * @return string the view file for the input form of the generator.
   */
  public function formView()
  {

    $class = new ReflectionClass($this);
    return dirname($class->getFileName()) . '/form.php';
  }

  /**
   * Returns the root path to the default code template files.
   * The default implementation will return the "templates" subdirectory of the
   * directory containing the generator class file.
   *
   * @return string the root path to the default code template files.
   */
  public function defaultTemplate()
  {

    $class = new ReflectionClass($this);
    return dirname($class->getFileName()) . '/default';
  }

  /**
   *
   * @return string the detailed description of the generator.
   */
  public function getDescription()
  {

    return '';
  }

  /**
   *
   * {@inheritdoc} Child classes should override this method like the following so that the parent
   *               rules are included:
   *
   *               ~~~
   *               return array_merge(parent::rules(), [
   *               ...rules for the child class...
   *               ]);
   *               ~~~
   */
  public function rules()
  {

    return [
      [
        [
          'template'
        ],
        'required',
        'message' => 'A code template must be selected.'
      ],
      [
        [
          'template'
        ],
        'validateTemplate'
      ]
    ];
  }

  /**
   * Loads sticky attributes from an internal file and populates them into the generator.
   *
   * @internal
   */
  public function loadStickyAttributes()
  {

    $stickyAttributes = $this->stickyAttributes();
    $path = $this->getStickyDataFile();
    if (is_file($path)) {
      $result = json_decode(file_get_contents($path), true);
      if (is_array($result)) {
        foreach ($stickyAttributes as $name) {
          if (isset($result[$name])) {
            $this->$name = $result[$name];
          }
        }
      }
    }
  }

  /**
   * Saves sticky attributes into an internal file.
   *
   * @internal
   */
  public function saveStickyAttributes()
  {

    $stickyAttributes = $this->stickyAttributes();
    $stickyAttributes[] = 'template';
    $values = [];
    foreach ($stickyAttributes as $name) {
      $values[$name] = $this->$name;
    }
    $path = $this->getStickyDataFile();
    @mkdir(dirname($path), 0755, true);
    file_put_contents($path, json_encode($values));
  }

  /**
   *
   * @return string the file path that stores the sticky attribute values.
   * @internal
   */
  public function getStickyDataFile()
  {

    return Yii::$app->getRuntimePath() . '/gii-' . Yii::getVersion() . '/' . str_replace('\\', '-', get_class($this)) . '.json';
  }

  /**
   * Saves the generated code into files.
   *
   * @param CodeFile[] $files
   *          the code files to be saved
   * @param array $answers
   * @param string $results
   *          this parameter receives a value from this method indicating the log messages
   *          generated while saving the code files.
   * @return bool whether files are successfully saved without any error.
   */
  public function save($files, $answers, &$results)
  {

    $lines = [
      'Generating code using template "' . $this->getTemplatePath() . '"...'
    ];
    $hasError = false;
    foreach ($files as $file) {
      $relativePath = $file->getRelativePath();
      if (isset($answers[$file->id]) && !empty($answers[$file->id]) && $file->operation !== CodeFile::OP_SKIP) {
        $error = $file->save();
        if (is_string($error)) {
          $hasError = true;
          $lines[] = "generating $relativePath\n<span class=\"error\">$error</span>";
        } else {
          $lines[] = $file->operation === CodeFile::OP_CREATE ? " generated $relativePath" : " overwrote $relativePath";
        }
      } else {
        $lines[] = "   skipped $relativePath";
      }
    }
    $lines[] = "done!\n";
    $results = implode("\n", $lines);
    $this->trigger(static::AFTER_SAVE_SUCCESS);
    return !$hasError;
  }

  /**
   *
   * @return string the root path of the template files that are currently being used.
   * @throws InvalidConfigException if [[template]] is invalid
   */
  public function getTemplatePath()
  {

    if (isset($this->templates[$this->template])) {
      return $this->templates[$this->template];
    }
    throw new InvalidConfigException("Unknown template: {$this->template}");
  }

  /**
   * Generates code using the specified code template and parameters.
   * Note that the code template will be used as a PHP file.
   *
   * @param string $template
   *          the code template file. This must be specified as a file path
   *          relative to [[templatePath]].
   * @param array $params
   *          list of parameters to be passed to the template file.
   * @return string the generated code
   */
  public function render($template, $params = [])
  {

    $view = new View();
    $params['generator'] = $this;
    return $view->renderFile($this->getTemplatePath() . '/' . $template, $params, $this);
  }

  /**
   * Validates the template selection.
   * This method validates whether the user selects an existing template
   * and the template contains all required template files as specified in [[requiredTemplates()]].
   */
  public function validateTemplate()
  {

    $templates = $this->templates;
    if (!isset($templates[$this->template])) {
      $this->addError('template', 'Invalid template selection.');
    } else {
      $templatePath = $this->templates[$this->template];
      foreach ($this->requiredTemplates() as $template) {
        if (!is_file(Yii::getAlias($templatePath . '/' . $template))) {
          $this->addError('template', "Unable to find the required code template file '$template'.");
        }
      }
    }
  }

  /**
   * An inline validator that checks if the attribute value refers to an existing class name.
   * If the `extends` option is specified, it will also check if the class is a child class
   * of the class represented by the `extends` option.
   *
   * @param string $attribute
   *          the attribute being validated
   * @param array $params
   *          the validation options
   */
  public function validateClass($attribute, $params)
  {

    $class = $this->$attribute;
    try {
      if (class_exists($class)) {
        if (isset($params['extends'])) {
          if (ltrim($class, '\\') !== ltrim($params['extends'], '\\') && !is_subclass_of($class, $params['extends'])) {
            $this->addError($attribute, "'$class' must extend from {$params['extends']} or its child class.");
          }
        }
      } else {
        $this->addError($attribute, "Class '$class' does not exist or has syntax error.");
      }
    } catch (\Exception $e) {
      $this->addError($attribute, "Class '$class' does not exist or has syntax error.");
    }
  }

  /**
   * An inline validator that checks if the attribute value refers to a valid namespaced class name.
   * The validator will check if the directory containing the new class file exist or not.
   *
   * @param string $attribute
   *          the attribute being validated
   * @param array $params
   *          the validation options
   */
  public function validateNewClass($attribute, $params)
  {

    $class = ltrim($this->$attribute, '\\');
    if (($pos = strrpos($class, '\\')) === false) {
      $this->addError($attribute, "The class name must contain fully qualified namespace name.");
    } else {
      $ns = substr($class, 0, $pos);
      $path = Yii::getAlias('@' . str_replace('\\', '/', $ns), false);
      if ($path === false) {
        $this->addError($attribute, "The class namespace is invalid: $ns");
      } elseif (!is_dir($path)) {
        $this->addError($attribute, "Please make sure the directory containing this class exists: $path");
      }
    }
  }

  /**
   * Checks if message category is not empty when I18N is enabled.
   */
  public function validateMessageCategory()
  {

    if ($this->enableI18N && empty($this->messageCategory)) {
      $this->addError('messageCategory', "Message Category cannot be blank.");
    }
  }

  /**
   *
   * @param string $value
   *          the attribute to be validated
   * @return bool whether the value is a reserved PHP keyword.
   */
  public function isReservedKeyword($value)
  {

    static $keywords = [
      '__class__',
      '__dir__',
      '__file__',
      '__function__',
      '__line__',
      '__method__',
      '__namespace__',
      '__trait__',
      'abstract',
      'and',
      'array',
      'as',
      'break',
      'case',
      'catch',
      'callable',
      'cfunction',
      'class',
      'clone',
      'const',
      'continue',
      'declare',
      'default',
      'die',
      'do',
      'echo',
      'else',
      'elseif',
      'empty',
      'enddeclare',
      'endfor',
      'endforeach',
      'endif',
      'endswitch',
      'endwhile',
      'eval',
      'exception',
      'exit',
      'extends',
      'final',
      'finally',
      'for',
      'foreach',
      'function',
      'global',
      'goto',
      'if',
      'implements',
      'include',
      'include_once',
      'instanceof',
      'insteadof',
      'interface',
      'isset',
      'list',
      'namespace',
      'new',
      'old_function',
      'or',
      'parent',
      'php_user_filter',
      'print',
      'private',
      'protected',
      'public',
      'require',
      'require_once',
      'return',
      'static',
      'switch',
      'this',
      'throw',
      'trait',
      'try',
      'unset',
      'use',
      'var',
      'while',
      'xor'
    ];
    return in_array(strtolower($value), $keywords, true);
  }

  /**
   * Generates a string depending on enableI18N property
   *
   * @param string $string
   *          the text be generated
   * @param array $placeholders
   *          the placeholders to use by `Yii::t()`
   * @param string $messageCategory
   *          the name be generated
   * @return string
   */
  public function generateString($string = '', $placeholders = [], $messageCategory = null)
  {

    if (empty($messageCategory)) {
      $messageCategory = $this->messageCategory;
    }
    $string = addslashes($string);
    if ($this->enableI18N) {
      // If there are placeholders, use them
      if (!empty($placeholders)) {
        $ph = ', ' . VarDumper::export($placeholders);
      } else {
        $ph = '';
      }
      $str = "Yii::t('" . $messageCategory . "', '" . $string . "'" . $ph . ")";
    } else {
      // No I18N, replace placeholders by real words, if any
      if (!empty($placeholders)) {
        $phKeys = array_map(function ($word) {
          return '{' . $word . '}';
        }, array_keys($placeholders));
        $phValues = array_values($placeholders);
        $str = "'" . str_replace($phKeys, $phValues, $string) . "'";
      } else {
        // No placeholders, just the given string
        $str = "'" . $string . "'";
      }
    }
    return $str;
  }

  /**
   * 获取项目的可能的翻译消息类文件
   *
   * @return string[]
   */
  public function getMessageCatetories()
  {

    $categories = [
      'app',
      'backend',
      'frontend',
      'api'
    ];
    // $dirs = FileHelper::findDirectories( \Yii::$app->basePath . '/Addons', [
    //     'recursive' => false
    // ] );
    $dirs = $this->getDevModeAddonNames();
    foreach ($dirs as $dir) {
      $addonName = basename($dir);
      $categories[] = 'addon_' . Inflector::camel2id($addonName, '_');
    }
    return $categories;
  }

  /**
   * 获取项目的可能的翻译消息类文件
   *
   * @return string[]
   */
  public function getMessageCatetoryPrefixs()
  {

    $prefixs = [
      'app',
      'backend',
      'frontend',
      'api'
    ];
    // $dirs = FileHelper::findDirectories( \Yii::$app->basePath . '/Addons', [
    //     'recursive' => false
    // ] );

    $dirs = $this->getDevModeAddonNames();
    foreach ($dirs as $dir) {
      $addonName = basename($dir);
      $prefixs[] = 'addon' . $addonName;
    }
    return $prefixs;
  }

  /**
   * 获取插件名称的所有列表
   *
   * @return array
   */
  public function getAddonNames()
  {

    // $dirs = FileHelper::findDirectories( \Yii::$app->basePath . '/Addons/', [
    //     'recursive' => false
    // ] );
    // return array_map( function ( $dir ) {
    //   return basename( $dir );
    // }, $dirs );

    return $this->getDevModeAddonNames();
  }

  public function getMessageCategoryPrefix($namespace)
  {

    if (substr(trim($namespace, '\\'), 0, 6) == 'Addons') {
      return 'da_';
    } else {
      return 'app_';
    }
  }

  public function getModelNamespaces()
  {

    $ns = [
      'Backend\\Models',
      'Frontend\\Models',
      'DuAdmin\\Models'
    ];
    // $dirs = FileHelper::findDirectories(\Yii::$app->basePath . '/Addons', [
    //   'recursive' => false
    // ]);

    $dirs = $this->getDevModeAddonNames();
    foreach ($dirs as $dir) {
      $addonName = basename($dir);
      $ns[] = 'Addons\\' . $addonName . '\\Models';
    }
    return $ns;
  }

  public function getWebControllerNamespaces()
  {

    $ns = [
      'Backend\\Controllers',
      'Frontend\\Controllers'
    ];
    // $dirs = FileHelper::findDirectories(\Yii::$app->basePath . '/Addons', [
    //   'recursive' => false
    // ]);

    $dirs = $this->getDevModeAddonNames();
    foreach ($dirs as $dir) {
      $addonName = basename($dir);
      $ns[] = 'Addons\\' . $addonName . '\\Controllers\\Backend';
      $ns[] = 'Addons\\' . $addonName . '\\Controllers\\Frontend';
    }
    return $ns;
  }

  public function getApiControllerNamespaces()
  {

    $ns = [];
    // $dirs = FileHelper::findDirectories(\Yii::$app->basePath . '/Addons', [
    //   'recursive' => false
    // ]);

    $dirs = $this->getDevModeAddonNames();
    foreach ($dirs as $dir) {
      $addonName = basename($dir);
      $ns[] = 'Addons\\' . $addonName . '\\Controllers\\Api';
    }
    return $ns;
  }

  public function getWebViewPathBases()
  {

    $ns = [
      '@Backend/Views',
      '@Frontend/Views'
    ];

    // $dirs = FileHelper::findDirectories(\Yii::$app->basePath . '/Addons', [
    //   'recursive' => false
    // ]);

    $dirs = $this->getDevModeAddonNames();
    foreach ($dirs as $dir) {
      $addonName = basename($dir);
      $ns[] = '@Addons/' . $addonName . '/Views/Backend';
      $ns[] = '@Addons/' . $addonName . '/Views/Frontend';
    }
    return $ns;
  }

  public function getDevModeAddonNames()
  {
    $configFile = \Yii::$app->basePath . '/Config/generator.php';
    if (file_exists($configFile)) {
      return require $configFile;
    }
    return [];
  }
}
